From 11e3ee75fcab0ab54b2bc1713a402c5cc3070c2d Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 1 Dec 2023 17:59:30 +0100
Subject: refactor(pages): refine Topic pages

* add useTopic and useTopicsList hooks to refresh data
* add a table of contents
* add Cypress tests
---
 src/pages/sujet/[slug].tsx | 202 +++++++++++++++++++++++++++++----------------
 1 file changed, 130 insertions(+), 72 deletions(-)

(limited to 'src/pages/sujet/[slug].tsx')

diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 185756b..8a9c2f3 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -15,17 +15,24 @@ import {
   PageHeader,
   PageSidebar,
   PageBody,
+  LoadingPage,
+  TocWidget,
+  Spinner,
 } from '../../components';
 import {
   convertWPTopicPreviewToPageLink,
-  convertWPTopicToTopic,
   fetchAllTopicsSlugs,
   fetchTopic,
   fetchTopicsCount,
   fetchTopicsList,
 } from '../../services/graphql';
 import styles from '../../styles/pages/blog.module.scss';
-import type { NextPageWithLayout, PageLink, Topic } from '../../types';
+import type {
+  GraphQLConnection,
+  NextPageWithLayout,
+  WPTopic,
+  WPTopicPreview,
+} from '../../types';
 import { CONFIG } from '../../utils/config';
 import { ROUTES } from '../../utils/constants';
 import {
@@ -34,21 +41,45 @@ import {
   getSchemaJson,
   getSinglePageSchema,
   getWebPageSchema,
+  slugify,
 } from '../../utils/helpers';
 import { loadTranslation, type Messages } from '../../utils/helpers/server';
-import { useBreadcrumb } from '../../utils/hooks';
+import {
+  useBreadcrumb,
+  useHeadingsTree,
+  useTopic,
+  useTopicsList,
+} from '../../utils/hooks';
 
 export type TopicPageProps = {
-  currentTopic: Topic;
-  topics: PageLink[];
+  data: {
+    currentTopic: WPTopic;
+    otherTopics: GraphQLConnection<WPTopicPreview>;
+    totalTopics: number;
+  };
   translation: Messages;
 };
 
-const TopicPage: NextPageWithLayout<TopicPageProps> = ({
-  currentTopic,
-  topics,
-}) => {
-  const { content, intro, meta, slug, title } = currentTopic;
+const TopicPage: NextPageWithLayout<TopicPageProps> = ({ data }) => {
+  const intl = useIntl();
+  const { isFallback } = useRouter();
+  const { isLoading, topic } = useTopic(
+    data.currentTopic.slug,
+    data.currentTopic
+  );
+  const { isLoading: areTopicsLoading, topics } = useTopicsList({
+    fallback: data.otherTopics,
+    input: { first: data.totalTopics, where: { notIn: [topic.id] } },
+  });
+  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+    title: topic.title,
+    url: `${ROUTES.TOPICS}/${topic.slug}`,
+  });
+  const { ref, tree } = useHeadingsTree({ fromLevel: 2 });
+
+  if (isFallback || isLoading) return <LoadingPage />;
+
+  const { content, intro, meta, slug, title } = topic;
   const {
     articles,
     cover,
@@ -57,17 +88,11 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
     relatedThematics,
     website: officialWebsite,
   } = meta;
-  const intl = useIntl();
-  const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
-    title,
-    url: `${ROUTES.TOPICS}/${slug}`,
-  });
 
-  const { asPath } = useRouter();
   const webpageSchema = getWebPageSchema({
     description: seo.description,
     locale: CONFIG.locales.defaultLocale,
-    slug: asPath,
+    slug,
     title: seo.title,
     updateDate: dates.update,
   });
@@ -78,30 +103,46 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
     id: 'topic',
     kind: 'page',
     locale: CONFIG.locales.defaultLocale,
-    slug: asPath,
+    slug,
     title,
   });
   const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
 
-  const topicsListTitle = intl.formatMessage({
-    defaultMessage: 'Other topics',
-    description: 'TopicPage: other topics list widget title',
-    id: 'JpC3JH',
-  });
-
-  const thematicsListTitle = intl.formatMessage({
-    defaultMessage: 'Related thematics',
-    description: 'TopicPage: related thematics list widget title',
-    id: '/sRqPT',
-  });
+  const messages = {
+    widgets: {
+      loadingTopicsList: intl.formatMessage({
+        defaultMessage: 'Topics are loading...',
+        description: 'TopicPage: loading topics message',
+        id: 'uUIgCr',
+      }),
+      thematicsListTitle: intl.formatMessage({
+        defaultMessage: 'Related thematics',
+        description: 'TopicPage: related thematics list widget title',
+        id: '/sRqPT',
+      }),
+      tocTitle: intl.formatMessage({
+        defaultMessage: 'Table of Contents',
+        description: 'PageLayout: table of contents title',
+        id: 'eys2uX',
+      }),
+      topicsListTitle: intl.formatMessage({
+        defaultMessage: 'Other topics',
+        description: 'TopicPage: other topics list widget title',
+        id: 'JpC3JH',
+      }),
+    },
+    browsePostsTitle: intl.formatMessage(
+      {
+        defaultMessage: 'Browse posts in {topicName} topic',
+        description: 'TopicPage: posts list heading',
+        id: 'd+DOFQ',
+      },
+      { topicName: title }
+    ),
+  };
 
-  const getPageHeading = () => (
-    <>
-      {cover ? <NextImage {...cover} className={styles['topic-logo']} /> : null}
-      {title}
-    </>
-  );
-  const pageUrl = `${CONFIG.url}${asPath}`;
+  const pageUrl = `${CONFIG.url}${slug}`;
+  const browsePostHeadingId = slugify(messages.browsePostsTitle);
 
   return (
     <Page breadcrumbs={breadcrumbItems}>
@@ -129,7 +170,14 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
         type="application/ld+json"
       />
       <PageHeader
-        heading={getPageHeading()}
+        heading={
+          <>
+            {cover ? (
+              <NextImage {...cover} className={styles['topic-logo']} />
+            ) : null}
+            {title}
+          </>
+        }
         intro={intro}
         meta={{
           publicationDate: dates.publication,
@@ -138,23 +186,33 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
           website: officialWebsite,
         }}
       />
-      <PageBody className={styles.body}>
-        {/*eslint-disable-next-line react/no-danger -- Necessary for content*/}
-        {content ? <div dangerouslySetInnerHTML={{ __html: content }} /> : null}
+      <PageSidebar>
+        <TocWidget
+          heading={<Heading level={2}>{messages.widgets.tocTitle}</Heading>}
+          tree={[
+            ...tree,
+            {
+              children: [],
+              depth: 2,
+              id: browsePostHeadingId,
+              label: messages.browsePostsTitle,
+            },
+          ]}
+        />
+      </PageSidebar>
+      <PageBody>
+        <div
+          className={styles.body}
+          // eslint-disable-next-line react/no-danger -- Necessary for content
+          dangerouslySetInnerHTML={{ __html: content }}
+          ref={ref}
+        />
         {articles ? (
           <>
-            <Heading level={2}>
-              {intl.formatMessage(
-                {
-                  defaultMessage: 'All posts in {topicName}',
-                  description: 'TopicPage: posts list heading',
-                  id: 'zEN3fd',
-                },
-                { topicName: title }
-              )}
+            <Heading id={browsePostHeadingId} level={2}>
+              {messages.browsePostsTitle}
             </Heading>
             <PostsList
-              className={styles.list}
               posts={getPostsWithUrl(articles)}
               headingLvl={3}
               sortByYear
@@ -166,21 +224,25 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
         {relatedThematics ? (
           <LinksWidget
             heading={
-              <Heading isFake level={3}>
-                {thematicsListTitle}
-              </Heading>
+              <Heading level={2}>{messages.widgets.thematicsListTitle}</Heading>
             }
             items={getLinksItemData(relatedThematics)}
           />
         ) : null}
-        <LinksWidget
-          heading={
-            <Heading isFake level={3}>
-              {topicsListTitle}
-            </Heading>
-          }
-          items={getLinksItemData(topics)}
-        />
+        {areTopicsLoading ? (
+          <Spinner>{messages.widgets.loadingTopicsList}</Spinner>
+        ) : (
+          <LinksWidget
+            heading={
+              <Heading level={2}>{messages.widgets.topicsListTitle}</Heading>
+            }
+            items={getLinksItemData(
+              topics.edges.map((edge) =>
+                convertWPTopicPreviewToPageLink(edge.node)
+              )
+            )}
+          />
+        )}
       </PageSidebar>
     </Page>
   );
@@ -198,23 +260,19 @@ export const getStaticProps: GetStaticProps<TopicPageProps> = async ({
 }) => {
   const currentTopic = await fetchTopic((params as TopicParams).slug);
   const totalTopics = await fetchTopicsCount();
-  const allTopicsEdges = await fetchTopicsList({
+  const otherTopics = await fetchTopicsList({
     first: totalTopics,
+    where: { notIn: [currentTopic.databaseId] },
   });
-  const allTopics = allTopicsEdges.edges.map((edge) =>
-    convertWPTopicPreviewToPageLink(edge.node)
-  );
-  const topicsLinks = allTopics.filter(
-    (topic) => topic.url !== `${ROUTES.TOPICS}/${(params as TopicParams).slug}`
-  );
   const translation = await loadTranslation(locale);
 
   return {
     props: {
-      currentTopic: JSON.parse(
-        JSON.stringify(convertWPTopicToTopic(currentTopic))
-      ),
-      topics: JSON.parse(JSON.stringify(topicsLinks)),
+      data: {
+        currentTopic: JSON.parse(JSON.stringify(currentTopic)),
+        otherTopics: JSON.parse(JSON.stringify(otherTopics)),
+        totalTopics,
+      },
       translation,
     },
   };
-- 
cgit v1.2.3

